8.7 关键字:explicit

构造函数与explicit

1. 拷贝构造函数不应该是explicit的

拷贝构造函数通常不应该是explicit的,因为它确实在一些情况下会被隐式地使用。

2. 转换构造函数应该是explicit的

我们可以将构造函数声明为explicit来阻止构造函数定义的隐式类类型转换,需要注意如下几点:

  • 关键字explicit只对一个实参的构造函数有效:需要多个实参的构造函数不能用于执行隐式转换,因此无需将这些构造函数指定为explicit

  • 只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应该重复

  • explicit声明的构造函数只能以直接初始化的形式使用

class Foo {
 public:
    // 阻止隐式类类型转换
    explicit Foo(std::string s) : str(s) { }
 private:
    std::string str;
}

std::string str = "tomocat";
// 错误: 不支持string到Foo类型的隐式初始化
Foo = str;
// 正确: 显式初始化
Foo(str);
static_cast<Foo>(str);

类型转换运算符与explicit

Tips:实践中类很少提供类型转换运算符,所以类型转换运算符常被explicit修饰以阻止隐式转换。

在实践中类很少提供类型转换运算符,在大多数情况下,如果类型转换自动发生,用户可能会感觉比较意外,而不是感觉受到了帮助。然而这条经验法则存在一种例外情况:对于类来说,定义向bool的类型转换还是比较普遍的现象。但是这种类型转换可能引发意想不到的结果,特别是当istream含有向bool的类型转换时,下面的代码仍然编译通过:

// 如果向bool的类型转换不是显式的,则该代码在编译器看来将是合法的
// 因为istream本身并没有定义<<运算符,所以本来这段代码应该产生错误
int i = 42;
cin << i;

// 执行过程如下:
// 1) istream的bool类型转换符将cin转换为bool
// 2) bool被提升为int并作为左移运算符的左侧运算对象
// 3) 提升后的bool值(1或0)会被左移42个位置

为了防止这样的异常发生,C++新标准引入了显式的类型转换运算符:

class SmallInt {
 public:
    // 转换构造函数: 编译支持int到SmallInt的隐式转换
    SmallInt(int i = 0) : val_(i) {
        if (i < 0 || i > 255)
            throw std::out_of_range("Bad SmallInt value");
    }
    // 类类型转换运算符: 编译器不支持SmallInt到int的隐式转换
    explicit operator int() const { return val_; }
    // ...其他成员

 private:
    int val_;
};

// 正确:SmallInt的构造函数不是显式的
SmallInt si = 3;
// 错误: 此处需要隐式的类型转换,但类的运算符是显式的
si + 3;
// 正确: 显式地请求类型转换
static_cast<int>(si) + 3;